#ifndef USER_H_
#define USER_H_

#include <string>
#include <memory>
#include <iostream>
#include <vector>
#include <set>
#include "recipe.h"
#include "mtm_ex5.h"
using std::string;
using std::shared_ptr;
using std::weak_ptr;
using std::set;
using std::ostream;

namespace mtm {

/*
 * prints the pointee value, instead of printing a shared_ptr address
 */
template<class T>
ostream& operator<<(ostream& os, const shared_ptr<T>& t) {
	return os << *t;
}

class PowerUser;
class Group;

class User {
protected:
	const string firstName;
	const string lastName;
	const unsigned id;
	const unsigned years; //assuming it won't change
	set<shared_ptr<Recipe>, CompareRecipes> recipes;
	weak_ptr<Group> group; //avoiding cyclic reference
public:
	/*
	 * open a new group, and became the group leader
	 * arguments:
	 * name - the name of the new group
	 * return value: pointer to a PowerUser, which represent the
	 * 		group opener (*this)
	 * throws:
	 * UserNotConnectedException, if the opener is not connected
	 * AlreadyMemberInGroupException, if the opener is already in a group
	 * GroupAlreadyExistsException, if there is already a group named name
	 */
	shared_ptr<PowerUser> openNewGroup(const string& name) const;
	/*
	 * ask to join in a group
	 * arguments:
	 * name - the name of the group to join
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * AlreadyMemberInGroupException, if the user is already in a group
	 * NoSuchGroupException, if there is not such group
	 */
	void joinNewGroup(const string& name) const;
	/*
	 * ask to leave group
	 * arguments:
	 * name - the name of the group to leave (should be
	 * 		same to the group that the user is in)
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * NotInGroupException, if the user is not in a group
	 * NoSuchGroupException, if there is not such group
	 */
	void leaveGroup(const string& name) const;
	/*
	 * let the user know if the leader accepted his join request
	 * arguments:
	 * accepted - true is the leader did accept. false if not.
	 * group - the group that the user asked to join
	 */
	void joinRequestCallback(bool accepted, shared_ptr<Group> group);
	/*
	 * let the user know if the leader accepted his leave request
	 * arguments:
	 * accepted - true is the leader did accept. false if not.
	 */
	virtual void leaveRequestCallback(bool accepted);
	/*
	 * post a new recipe in the current group
	 * arguments:
	 * recipeName - the new recipe's name
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * NotInGroupException, if the user is not in a group
	 * RecipeAlreadyExistsException, if the recipe has already published in group
	 */
	void postRecipe(const string& recipeName);
	/*
	 * add recipe (from the current group) to the user's collection
	 * arguments:
	 * recipeName - the recipe's name
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * NotInGroupException, if the user is not in a group
	 * NoSuchRecipeInGroupException - if there is not such recipe in group
	 */
	void acquireRecipe(const string& recipeName);
	/*
	 * drop a recipe from the user's collection
	 * arguments:
	 * chefId - the id of the recipe's publisher
	 * recipeName - the name of the recipe
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * NotInGroupException, if the user is not in a group
	 * AppreciateYourOwnWorkException, if the user is the recipe's publisher
	 * NoSuchRecipeInCollectionException, if the recipe is not in the collection
	 */
	void forgetRecipe(unsigned chefId, const string& recipeName);
	/*
	 * get the numbers of users whose collection contains
	 * one of the user's recipes.
	 * (if a collection contains some recipes, it's being
	 * counted multiple times)
	 * return value:
	 * that total amount.
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * NotInGroupException, if the user is not in a group
	 */
	int getFollowersAmount() const;
	User(const string& firstName, const string& lastName, int id, int years) :
			firstName(firstName), lastName(lastName), id(id), years(years) {
	}

	/*
	 * getters for User:
	 */

	unsigned getId() const {
		return id;
	}
	unsigned getYears() const {
		return years;
	}
	const string& getFirstName() const {
		return firstName;
	}
	const string& getLastName() const {
		return lastName;
	}
	shared_ptr<Group> getGroup() const {
		return group.lock();
	}
	/*
	 * get the number of request that the user published in
	 * the current group
	 * Note: this function uses shared_ptr's internal counter,
	 * so it won't work if you save shared_ptr to recipes where they
	 * should not be counted
	 */
	int getDonation() const;
	virtual ~User() {
	}

	//some functions that should be called only with a PowerUser
	virtual shared_ptr<PowerUser> appointNewLeader() {
		throw IllegalArguments();
	}
	virtual void inspectJoinRequest() {
	}
	virtual void inspectLeaveRequest(ostream&) {
	}
	//until here

	/*
	 * prints user's fields
	 * arguments:
	 * os - the stream to print to
	 * user - the User to print
	 * return value: os
	 */
	friend std::ostream& operator<<(std::ostream& os, const User& user);
};

//Leader of group
class PowerUser: public User {
	//keep the last uninspected requests
	weak_ptr<User> joinRequestUser, leaveRequestUser;
public:
	/*
	 * appoint a new leader
	 * find the most donating user in the group, and turn it
	 * to a group leader
	 * return value: a pointer to PowerUser, which represent the
	 * 		new leader
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * NotInGroupException, if the user is not in a group
	 * ForeverAloneException, if there is not another user in group
	 */
	virtual shared_ptr<PowerUser> appointNewLeader();
	/*
	 * inspect a join request
	 * the request will be approved, if (and only if) the asking user
	 * is not newer than the leader.
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * NotInGroupException, if the user is not in a group
	 * NoAwaitingJoinRequestException, if there is not such a request
	 */
	virtual void inspectJoinRequest();
	/* inspect a leave request
	 * the request will always be approved.
	 * if the asking user is not the leader, he will be cursed.
	 * arguments:
	 * os - the stream to print the curse to.
	 * throws:
	 * UserNotConnectedException, if the user is not connected
	 * NotInGroupException, if the user is not in a group
	 * NoAwaitingLeaveRequestException, if there is not such a request
	 */
	virtual void inspectLeaveRequest(ostream& os);
	PowerUser(const User& user) :
			User(user) {
	}

	friend class Group;
	/*
	 * same as User::leaveRequestCallback, but also
	 * may appoint a new leader
	 */
	virtual void leaveRequestCallback(bool accepted);
};

}

#endif
